package com.gitee.chili.leisure.util;

import cn.hutool.json.JSONUtil;
import com.gitee.chili.leisure.Constants;
import com.gitee.chili.leisure.RoomManager;
import com.gitee.chili.leisure.Tools;
import com.gitee.chili.leisure.core.Packet;
import com.gitee.chili.leisure.domain.dto.SpecialCards;
import com.gitee.chili.leisure.enums.CardType;
import com.gitee.chili.leisure.enums.GameType;
import com.gitee.chili.leisure.enums.State;
import com.gitee.chili.leisure.game.MahjongPlayer;
import com.gitee.chili.leisure.game.Player;
import com.gitee.chili.leisure.game.RoomDetail;
import com.gitee.chili.leisure.message.ExchangeThree;
import com.gitee.chili.leisure.model.BaseMessageModel;
import com.gitee.chili.leisure.model.HallModel;
import com.gitee.chili.leisure.model.MahjongModel;
import com.gitee.chili.leisure.model.MahjongModel.OptionsType;
import com.gitee.chili.leisure.model.MessageCmdEnum;
import lombok.extern.slf4j.Slf4j;

import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.LongAdder;

/**
 * 房间辅助类
 *
 * @author week solider
 * @since 2020.09.03
 */
@Slf4j
public final class AuxiliaryRoom {


    /**
     * 构造房间信息结构
     *
     * @param roomDetail 房间信息
     * @param ip         ip地址
     */
    public static HallModel.NotifyRoomResult.Builder notifyRoom(RoomDetail roomDetail, String ip) {

        // 玩家信息
        List<HallModel.RoomMember> members = new ArrayList<>();

        // 构造玩家信息
        for (Player p : roomDetail.getPlayers()) {
            p.setIp(ip);
            members.add(HallModel.RoomMember.newBuilder()
                    .setPlayerState(HallModel.PlayerState.NOT_READY)
                    .setAssets(Tools.rmbCast(p.getScore()))
                    .setMemberName(p.getNickname())
                    .setPortrait(p.getPortrait())
                    .setSeatNum(p.getPosition())
                    .setMemberId(p.getUserId())
                    .setGender(p.getGender())
                    .setIp(ip)
                    .build());
        }

        // 房间等级信息
        HallModel.NotifyRoomResult.Grade grade = HallModel.NotifyRoomResult.Grade.newBuilder()
                .setBaseScore(roomDetail.getBaseScore())
                .setCappedPoints(roomDetail.getCappedPoints())
                .setLowerScore(roomDetail.getLowerScore())
                .setUpperScore(roomDetail.getUpperScore())
                .build();

        // 房间信息
        HallModel.NotifyRoomResult.Builder builder = HallModel.NotifyRoomResult.newBuilder();
        builder.setRules(JSONUtil.toJsonStr(roomDetail.getRules()))
                .setCreateTime(roomDetail.getCreateTime().getTime())
                .setRoomType(roomDetail.getRoomType().getCode())
                .setPeopleNum(roomDetail.getPlayers().size())
                .setHomeowner(roomDetail.getHomeowner())
                .setPay(roomDetail.getPay().getCode())
                .setGameType(roomDetail.getGameType())
                .setRoomNum(roomDetail.getRoomNum())
                .setInnings(roomDetail.getInnings())
                .setGrade(grade);

        // 添加玩家信息
        members.forEach(m -> builder.setMembers(m.getSeatNum(), m));

        return builder;
    }

    /**
     * 玩家离线
     */
    public static Packet off() {
        MahjongModel.PlayerOffResult.Builder builder = MahjongModel.PlayerOffResult.newBuilder();
        return Packet.newPacket(MessageCmdEnum.Command.PLAYER_OFF_LINE, builder.build().toByteArray());
    }

    /**
     * 解散房间
     *
     * @param roomNum 房间号
     * @return 解散房间数据包
     */
    public static Packet dissolved(Long roomNum) {
        // 清空缓存
        RoomManager.end(roomNum);
        Connection.broadcaster().remove(roomNum);

        HallModel.DissolvedRoomResult.Builder builder = HallModel.DissolvedRoomResult.newBuilder();
        return Packet.newPacket(MessageCmdEnum.Command.DISSOLVED_ROOM, builder.build().toByteArray());
    }

    /**
     * 推送发牌数据
     *
     * @param roomNum  房间号
     * @param memberId 玩家id
     */
    public static Packet pushDealData(Long roomNum, Long memberId) {
        RoomDetail roomDetail = RoomManager.obtain(roomNum);
        Player player = roomDetail.obtain(memberId);

        MahjongModel.DealCardsResult.Builder builder = MahjongModel.DealCardsResult.newBuilder();
        builder.setBalanceCards(roomDetail.getRestCardsNum())
                .setScore(Tools.rmbCast(player.getScore()))
                .setBankerSeat(roomDetail.getBankerSeat())
                .addAllHoldCards(player.getHandsCards())
                .setSeatNum(player.getPosition())
                .setPlayerId(player.getUserId())
                .setDiceSecond(Tools.shaker())
                .setDiceFirst(Tools.shaker())
                .setSpecialCards(-1);

        return Packet.newPacket(MessageCmdEnum.Command.DEAL_CARDS, builder.build().toByteArray());
    }

    /**
     * 发起开始换三张消息
     */
    public static Packet launchExchange() {
        MahjongModel.ExchangeStart.Builder builder = MahjongModel.ExchangeStart.newBuilder();
        return Packet.newPacket(MessageCmdEnum.Command.START_CHANGING, builder.build().toByteArray());
    }

    /**
     * 开始游戏
     */
    public static Packet begin() {
        HallModel.NotifyRoomBeginResult.Builder builder = HallModel.NotifyRoomBeginResult.newBuilder();
        return Packet.newPacket(MessageCmdEnum.Command.NOTIFY_ROOM_BEGIN, builder.build().toByteArray());
    }

    /**
     * 退出房间
     */
    public static void exit(Long roomNum, Long memberId) {
        // 清除缓存
        RoomManager.exit(roomNum, memberId);
        Connection.broadcaster().exit(roomNum, memberId);
    }

    /**
     * 发起开始定缺消息
     */
    public static Packet launchUseless() {
        MahjongModel.StartUseless.Builder builder = MahjongModel.StartUseless.newBuilder();
        return Packet.newPacket(MessageCmdEnum.Command.START_USELESS, builder.build().toByteArray());
    }

    /**
     * 添加可操作类型
     *
     * @param type  操作类型(如：吃碰杠)
     * @param cards 可操作的牌
     */
    public static MahjongModel.CanOperateResult.OperateModel.Builder canOperate(OptionsType type, List<Integer> cards) {
        MahjongModel.CanOperateResult.OperateModel.Builder omb = MahjongModel.CanOperateResult.OperateModel.newBuilder();
        return omb.setType(type).addAllCard(cards);
    }

    /**
     * 通知出牌
     *
     * @param player 房间号
     */
    public static MahjongModel.CanOperateResult.Builder notifiesDiscard(Player player) {

        MahjongModel.CanOperateResult.OperateModel.Builder omb = canOperate(OptionsType.DISCARD, player.getHandsCards());

        MahjongModel.CanOperateResult.Builder builder = MahjongModel.CanOperateResult.newBuilder();
        builder.setSeatNum(player.getPosition()).addOperateModel(omb);

        return builder;
    }

    /**
     * 断线重连
     *
     * @param roomNum 房间号
     */
    public static Packet reconnection(Long roomNum) {
        MahjongModel.Disconnected.Builder builder = MahjongModel.Disconnected.newBuilder();
        builder.setRoomNum(roomNum);
        return Packet.newPacket(MessageCmdEnum.Command.DISCONNECTED, builder.build().toByteArray());
    }

    /**
     * 出牌
     */
    public static Packet discard(MahjongPlayer player, MahjongModel.PlayerOperateResult operateResult) {

        // 房间信息
        RoomDetail info = RoomManager.obtain(player.getRoomNum());

        // 是否轮到自己出牌
        if (info.getDiscardSeat() == player.getPosition()) {

            // 被操作的牌
            int beOperateCard = operateResult.getOperateInfoModel().getBeOperateCard();

            // 获取游戏类型
            GameType type = GameType.valueOf(info.getGameType().intValue());

            if (type.equals(GameType.XZ) && player.isWined()) {
                // 已经胜利了，血战中胡牌后不可再出牌、摸牌
                log.error("血战到底模式胡牌后不可再出牌 ， 玩家id: {}", player.getUserId());
                return null;
            }

            // 手牌中没找到要出的牌
            if (!player.getHandsCards().contains(beOperateCard)) {
                // 没有找到牌的信息
                log.warn("====================== 在手牌中没有找到出牌 ==================");
                log.warn("时间: {}", System.currentTimeMillis());
                log.warn("编号: {}", player.getUserId());
                log.warn("房号: {}", player.getRoomNum());
                log.warn("手牌: {}", player.getHandsCards());
                log.warn("出牌: {}", beOperateCard);
                log.warn("=============================================================");
                return null;
            }

            // 判断是否还有定缺牌没有打完  没打完为 false
            if (AuxiliaryRoom.isUselessClear(player)) {
                // 检查听牌
                if (isReadyHand(player, beOperateCard)) {

                    // 听牌及番数提示
                    MahjongModel.ReadyHandTipsResult.Builder builder = MahjongModel.ReadyHandTipsResult.newBuilder();

                    List<MahjongModel.ReadyHandTipsResult.Tips> tbs = new ArrayList<>();
                    player.getReadyHandMap().forEach((k, v) ->
                            tbs.add(MahjongModel.ReadyHandTipsResult.Tips.newBuilder()
                                    .setReadyHand(k)
                                    .setPoints(v)
                                    .build()));

                    builder.addAllTips(tbs);

                    // 玩家听牌
                    player.setReadyHand(true);

                    // 推送听牌提示
                    Connection.broadcaster().send(player.getRoomNum(), player.getUserId(),
                            Packet.newPacket(MessageCmdEnum.Command.READY_HAND_TIPS, builder.build().toByteArray()));
                }
            }

            // 出牌
            player.discard(beOperateCard);

            // TODO 这里应该要记录玩家的出牌过程 (先不做) 如果需要实现一定要使用缓存进行存储，牌局结束时，进行持久化操作

            // 返回出牌数据
            return Packet.newPacket(MessageCmdEnum.Command.OPERATE_RESULT, operateResult.toByteArray());
        }

        return null;
    }

    /**
     * 判断定缺牌是否打光
     *
     * @param player 玩家信息
     */
    private static boolean isUselessClear(MahjongPlayer player) {
        return player.getHandsCards().stream().noneMatch(p -> AdaptionAlgorithm.typeOf(p).equals(player.getUseless()));
    }

    /**
     * 验证杠和碰的操作
     */
    public static void checkSpecificCardAndAdd(MahjongModel.CanOperateResult.Builder builder, List<Integer> newHandCard,
                                               Integer card, MahjongPlayer player) {
        // 检查碰
        if (AdaptionAlgorithm.checkPong(newHandCard, card)) {
            builder.setSeatNum(player.getPosition()).addOperateModel(AuxiliaryRoom.canOperate(MahjongModel.OptionsType.PONG,
                    Collections.singletonList(card)));

            player.setPong(Constants.YES);
        }

        // 检查杠
        if (AdaptionAlgorithm.checkKong(newHandCard, card)) {
            builder.setSeatNum(player.getPosition()).addOperateModel(AuxiliaryRoom.canOperate(MahjongModel.OptionsType.KONG,
                    Collections.singletonList(card)));

            player.setKong(Constants.YES);
        }
    }

    /**
     * 构造玩家操作消息
     *
     * @param builder 数据对象
     */
    public static Packet pushOperate(MahjongModel.CanOperateResult.Builder builder) {
        return Packet.newPacket(MessageCmdEnum.Command.CAN_OPERATE, builder.build().toByteArray());
    }

    /**
     * 胡牌
     *
     * @param player        玩家信息 (胡牌的)
     * @param operateResult 操作信息
     */
    public static Packet winner(MahjongPlayer player, MahjongModel.PlayerOperateResult operateResult) {

        // 被操作的牌
        int beOperateCard = operateResult.getOperateInfoModel().getBeOperateCard();

        // 如果玩家已经听牌 检查玩家听牌
        if (player.isReadyHand() || isReadyHand(player, beOperateCard)) {
            winnerSettlement(player, beOperateCard, operateResult.getSeatNum());
        }

        return Packet.newPacket(MessageCmdEnum.Command.OPERATE_RESULT, operateResult.toByteArray());
    }

    /**
     * 胡牌结算
     *
     * @param player       玩家
     * @param discard      胡牌
     * @param loserSeatNum 放炮位置 (自摸时是: 99)
     */
    private static void winnerSettlement(MahjongPlayer player, Integer discard, int loserSeatNum) {

        Map<Integer, Integer> handMap = player.getReadyHandMap();

        // 听牌信息中必须要包含这张牌
        if (handMap.containsKey(discard)) {
            player.setWined(true);
            // 番数
            Integer points = handMap.get(discard);
            // 房间信息
            RoomDetail roomDetail = RoomManager.obtain(player.getRoomNum());
            // 要扣除的分数
            long loserScore = roomDetail.getBaseScore() * points;

            List<? extends Player> players = roomDetail.getPlayers();

            MahjongModel.OperateSettlementOrder.Builder builder = MahjongModel.OperateSettlementOrder.newBuilder();
            MahjongModel.OperateSettlementOrder.Loser.Builder loserBuilder = MahjongModel.OperateSettlementOrder.Loser.newBuilder();

            // 代表自摸
            if (loserSeatNum == 99) {
                // 自摸分数翻倍
                long selfDrawScore = loserScore * 2;
                // 计算在场玩家
                players.forEach(p -> criticalCompute(player, p, selfDrawScore, loserBuilder));
            } else {

                // 计算分数
                Utils.accept(players, p -> p.getPosition() == loserSeatNum, p -> criticalCompute(player, p, loserScore, loserBuilder));
            }
            builder.addLoser(loserBuilder);

            Packet packet = Packet.newPacket(MessageCmdEnum.Command.OPERATE_SETTLEMENT_ORDER, builder.build().toByteArray());

            // 广播结算清单
            Connection.broadcaster().notify(player.getRoomNum(), packet);
        }

        log.error("玩家胡牌信息中不包含这张牌, 房间号: {} , 玩家Id: {}", player.getRoomNum(), player.getUserId());

        // 构造错误信息
        BaseMessageModel.Error error = AuxiliaryChannel.errorMessage(MessageCmdEnum.Command.OPERATE_SETTLEMENT_ORDER, 500, "结算失败");

        // 广播错误消息
        Connection.broadcaster().notify(player.getRoomNum(), Packet.newPacket(MessageCmdEnum.Command.ERROR, error.toByteArray()));
    }

    /**
     * 临界值计算 (防止不够付的情况)
     *
     * @param winner     赢家
     * @param loser      输家
     * @param loserScore 输的分
     */
    public static void criticalCompute(Player winner, Player loser, long loserScore,
                                       MahjongModel.OperateSettlementOrder.Loser.Builder loserBuilder) {

        if (loser.getScore().compareTo(new BigDecimal(String.valueOf(loserScore))) > 0) {
            loser.subScore(loserScore);
            winner.addScore(loserScore);

            // 添加清单数据
            loserBuilder.setScore(loserScore)
                    .setSeatNum(loser.getPosition());
        } else {

            // 不够付的情况  清空掉输掉的玩家剩余分数 赢的玩家加上所有分数
            winner.addScore(loser.getScore());
            loser.setScore(BigDecimal.ZERO);

            // 宣布破产
            loser.setStatus(State.loser);

            loserBuilder.setScore(Tools.rmbCast(loser.getScore()))
                    .setSeatNum(loser.getPosition());
        }
    }

    /**
     * 判断玩家是否能听牌
     *
     * @param player  玩家信息
     * @param discard 打出的牌
     */
    public static boolean isReadyHand(MahjongPlayer player, Integer discard) {
        // 手牌
        List<Integer> newHandCards = new ArrayList<>(player.getHandsCards());
        // 台面上的牌
        List<Integer> newSpecialCards = new ArrayList<>();
        // 移除掉这张牌后的听牌计算
        newHandCards.remove(discard);

        // 统计台面上的牌
        List<SpecialCards> specialCards = player.getSpecialCards();
        if (Utils.nonEmpty(specialCards)) {
            specialCards.forEach(sc -> newSpecialCards.addAll(sc.getCards()));
        }
        // 计算听牌信息
        List<Integer> readyHand = AdaptionAlgorithm.checkedReadyHand(newHandCards);

        // 注意: 这里赋值只是为了方便 没有实际作用
        player.setReadyHandMap(AdaptionAlgorithm.calculationPoints(newHandCards, newSpecialCards, readyHand));

        return Utils.nonEmpty(readyHand);
    }

    /**
     * 检查听牌
     *
     * @param player 玩家信息
     * @return 打出能听的牌
     */
    public static List<Integer> checkReadyHand(MahjongPlayer player, MahjongModel.CanOperateResult.Builder builder) {
        // 不破坏原结构
        List<Integer> newHandCard = new ArrayList<>(player.getHandsCards());

        // 记录上一次的检查指针 (上一次移除的牌) 保证容错 初始化为 -1
        AtomicInteger preRemoveCard = new AtomicInteger(-1);

        // 打出能听的牌
        List<Integer> canReadyHand = new ArrayList<>();

        newHandCard.forEach(hc -> {
            if (hc.equals(preRemoveCard.get())) {
                // 这里不会结束循环 会进行下一次
                return;
            }
            preRemoveCard.set(hc);
            newHandCard.remove(hc);

            // 听牌信息
            List<Integer> readyHand = AdaptionAlgorithm.checkedReadyHand(newHandCard);

            // 听牌番数
            if (!readyHand.isEmpty()) {
                if (readyHand.contains(hc)) {
                    // 自摸 (如果是庄第一次出牌就是天胡)
                    builder.addOperateModel(canOperate(OptionsType.WINNER, Collections.singletonList(hc)));
                }
                canReadyHand.add(hc);
            }
        });

        player.setDiscardCanReadyHand(canReadyHand);

        return canReadyHand;
    }

    /**
     * 换三张
     *
     * @param direction 方向
     * @param players   所有玩家
     */
    public static void exchangeThree(ExchangeThree.Direction direction, List<? extends Player> players) {
        switch (direction) {
            case above:
                for (int i = 0; i < players.size(); i++) {
                    int seat = (i + 1) % 4;
                    exchange(players.get(seat), ((MahjongPlayer) players.get(i)).getExchange());
                }
            case below:
                for (int i = 0; i < players.size(); i++) {
                    int seat = (i + 1) % 4;
                    exchange(players.get(i), ((MahjongPlayer) players.get(seat)).getExchange());
                }
            case opposite:
                for (int i = 0; i < players.size(); i++) {
                    int seat = i > 1 ? i + 2 : i - 2;
                    exchange(players.get(i), ((MahjongPlayer) players.get(seat)).getExchange());
                }
        }
    }

    /**
     * 进行交换 (换三张)
     *
     * @param player 玩家
     * @param cards  换到的手牌
     */
    private static void exchange(Player player, List<Integer> cards) {
        cards.forEach(player::draw);
    }

    /**
     * 推送换三张后的手牌数据
     *
     * @param player 玩家信息
     */
    public static Packet pushExchangedData(Player player) {
        // 推送换牌中的手牌给玩家
        MahjongModel.Exchanging.Builder builder = MahjongModel.Exchanging.newBuilder();
        builder.setSeatNum(player.getPosition()).addAllHandCards(player.getHandsCards());
        return Packet.newPacket(MessageCmdEnum.Command.EXCHANGING, builder.build().toByteArray());
    }

    /**
     * 换牌是否结束
     *
     * @param players 所有玩家信息
     */
    public static boolean exchangeOver(List<? extends Player> players) {
        return players.stream().noneMatch(p -> ((MahjongPlayer) p).getExchange().isEmpty());
    }

    /**
     * 获取换三张最优的类型
     *
     * @param player 玩家信息
     */
    public static CardType optimalExchangeType(Player player) {

        LongAdder character = new LongAdder();
        LongAdder bamboo = new LongAdder();
        LongAdder dot = new LongAdder();

        // 统计每张牌的类型 数目
        player.getHandsCards().forEach(card -> {

            switch (AdaptionAlgorithm.typeOf(card)) {
                case character:
                    character.increment();
                    break;
                case bamboo:
                    bamboo.increment();
                    break;
                case dot:
                    dot.increment();
                    break;
            }
        });

        long characterSum = character.sum();
        long bambooSum = bamboo.sum();
        long dotSum = dot.sum();

        CardType exchangeType = null;

        // 找到最优的类型
        if (characterSum > 2) {
            if (characterSum < bambooSum && characterSum < dotSum) {
                exchangeType = CardType.character;
            }
        } else if (bambooSum > 2) {
            if (bambooSum < dotSum) {
                exchangeType = CardType.bamboo;
            }
        } else {
            exchangeType = CardType.dot;
        }

        return exchangeType;
    }

    /**
     * 换三张结束
     *
     * @param roomNum   房间号
     * @param direction 方向
     */
    public static void exchangeFinish(Long roomNum, ExchangeThree.Direction direction) {
        // 推送换三张结束消息
        RoomManager.obtain(roomNum).getPlayers().forEach(player -> {
            MahjongModel.ExchangeFinish.Builder builder = MahjongModel.ExchangeFinish.newBuilder();
            builder.setDirection(direction.ordinal())
                    .setSeatNum(player.getPosition())
                    .addAllHandCards(player.getHandsCards());
            Packet packet = Packet.newPacket(MessageCmdEnum.Command.END_CHANGING, builder.build().toByteArray());
            Connection.broadcaster().send(player.getRoomNum(), player.getUserId(), packet);
        });
    }

    /**
     * 检查定缺是否结束
     *
     * @param players 所有玩家信息
     */
    public static boolean uselessOver(List<? extends Player> players) {
        return players.stream().noneMatch(p -> ((MahjongPlayer) p).getUseless().equals(CardType.undefined));
    }

    /**
     * 定缺完成
     *
     * @param players 所有玩家信息
     */
    public static Packet uselessFinish(List<? extends Player> players) {

        List<MahjongModel.EndUseless.UselessInfo.Builder> ubs = new ArrayList<>();

        MahjongModel.EndUseless.UselessInfo.Builder ub;

        for (Player p : players) {
            MahjongPlayer mp = (MahjongPlayer) p;
            ub = MahjongModel.EndUseless.UselessInfo.newBuilder();
            ub.setSeat(mp.getPosition()).setType(mp.getUseless().ordinal());
            ubs.add(ub);
        }

        MahjongModel.EndUseless.Builder builder = MahjongModel.EndUseless.newBuilder();
        ubs.forEach(u -> builder.addUselessInfo(u.getSeat(), u));
        return Packet.newPacket(MessageCmdEnum.Command.END_USELESS, builder.build().toByteArray());
    }

    /**
     * 过
     *
     * @param build 玩家操作对象
     */
    public static Packet pass(MahjongModel.PlayerOperateResult build) {
        return Packet.newPacket(MessageCmdEnum.Command.OPERATE_RESULT, build.toByteArray());
    }

    /**
     * 下一个出牌的玩家位置 （这里只是进行了阈值处理 并没有实际的处理玩家操作）
     *
     * @return 轮上就是上一个位置加一的操作，但有可能中间有人进行碰、杠进行了位置转移，且位置三过后是位置一
     */
    public static int nextDealer(Player currentDealer) {

        // 玩家列表
        List<? extends Player> players = RoomManager.obtain(currentDealer.getRoomNum()).getPlayers();

        // 计算下一个摸牌的玩家
        AtomicInteger next = new AtomicInteger(currentDealer.getPosition());
        int seat = next.incrementAndGet() % players.size();

        // 如果玩家已经破产则自己跳到下一个
        if (players.get(seat).getStatus().equals(State.loser)) {
            return nextDealer(players.get(next.incrementAndGet()));
        }

        return seat;
    }
}
